""" This module implements the resource management system (RMS).
    FIXME: Please see the wiki for more information.
"""

import os.path
import posixpath
import weakref
from error import ResourceError
import debug

__all__ = (
    'init',
    'register_type',
    'get',
    'depend',
    'get_metadata',
    'get_exists',
    'get_type',
    'set_dir',
    'add_metadata',
    'switch_library',
    'add_library',
    'MetaData'
)

_init = False # Whether or not the module is initialized

def init():
    """ Initializes the resource management system.
        Must be called before using the resource management system
    """
    global _init, _metadata, _cwd, _type
    if not _init:
        _init = True
        _metadata = {} # Maps the metadata IDs to metadata objects
        _cwd = '' # current ID prefix
        _type = {} # Maps the resource type IDs to autoloader functions

def _register_metadata(metadata):
    """ Internal function.
        Registers an metadata with the manager.
    """
    try:
        theID = metadata.id
    except AttributeError:
        raise ResourceError,"Object being registered does not have a resource ID."
    if theID in _metadata:
        raise ResourceError,'Duplicate registration of resource ID "%s".'%theID
    _metadata[theID] = metadata

# _unregister_metadata() ?

def register_type(typeID, autoLoader):
    """ Registers the given resource type.
        
        Pass a string for the typeID and a reference to an automatic load
        function. When a reference to an unloaded resource of the given type is
        requested, the autoloader will be called with a reference to the metadata
        for the resource. The function should return a reference to a new
        resource object.
        
        Raises TypeError if you pass a typeID that is not a string.
        
        Raises ValueError if you attempt to register with an ID that is
        already in use.
    """
    if not isinstance(typeID,str):
        raise TypeError("typeID must be a string, got %s"%typeID)
    if typeID in _type:
        raise ValueError("Resource type ID '%s' already in use."%typeID)
    _type[typeID] = autoLoader

# TODO: unregister_type() ?

def switch_library(lib):
    """ Replaces the current set of metadatas with new ones created from a library.
        
        A "library" is simply a dictionary that maps resource IDs to another
        dictionary containing information needed for loading the resource.
        
        Example:
        
        >>> lib = {'someImage': {'type':'image', src:'image.png'},
        >>>        'someWorld': {'type':'world', src:'world.rwd'}}
        >>> switch_library(lib)
        
        This function is only intended to be used when there are no resources in
        the registry because MetaDatas are not "told" that they are being removed
        from the RMS. MetaDatas that used to be in the RMS will behave as
        if they are still in the RMS.
    """
    # The code below could easily be simplified to a single assignment but I'm
    # doing it in a round-about sort of way in case there is a second reference
    # to the _metadata dictionary somewhere. Also, it helps verify the new library.
    
    # Clear the metadata dictionary
    for k in _metadata.keys():
        del _metadata[k]
    # Add each new item in the lib
    # NOTE: This is coded in a very optimized way even though it's probably not
    # necessary. Call it an experiment. :-D
    map(_register_metadata, [MetaData(k,**v) for k,v in lib.iteritems()] )

def add_library(lib):
    """ Adds a new library to the RMS.
        
        This works the same way as switch_library(lib) except that old metadatas
        are preserved. Also, the current id prefix will be added to the IDs of all
        resource metadatas added.
        
        If there are any conflicting IDs sbak.error.ResorceError will be raised
        and nothing will be added.
    """
    metadatas = _metadata
    # FIXME: Find a more efficient way to check for duplicates
    libNames = dict([(n, get_abs_id(n)) for n in lib.keys()])
    for oldN,newN in libNames.iteritems():
        if newN in metadatas:
            raise ResourceError("Duplicate ID found: '%s' (as '%s')"%(oldN,newN))
    # Add new library to manager
    for oldN,res in lib.iteritems():
        _register_metadata(MetaData(libNames[oldN],**res))

def set_pre(*d):
    """ Changes the prefix assumed to be on all IDs.
    
        This function works similarly to the "change directory" command found
        on Posix systems, but it doesn't actually change the program's
        current working directory. Instead, it sets a prefix which is assumed to
        be on all IDs used within the resource manager. This is useful if you
        want to group related resources but don't want to type the same prefix
        onto each one of them.
        
        Example:
        
        >>> # Stub a resource with ID "A"
        >>> resource.stub('A','surface','resources/A.png') # Resource with id 'A'
        >>> # Change the prefix
        >>> resource.set_pre('prefix','prefix2')
        >>> # Stub another resource with ID "prefix/prefix2/A"
        >>> resource.stub('A','surface','resources/otherA.png')
        
        Notice that in the example above the second stub does NOT cause an
        exception because the prefix set before is prepended to the ID.
        
        Without resetting the prefix to the default '' (empty string) the
        two resources can be accessed as follows:
        
        >>> # Get the first resource
        >>> a = resource.get('../../A')
        >>> # Get the second resource
        >>> b = resource.get('A')
        
        The ID prefix allows for a crude emulation of a filesystem. However,
        please take care to ensure that the prefix is always reset to the
        "base directory" after you are done in a psuedo-directory, otherwise
        other parts of the program may not be able to find the resources
        as expected.
        
        Example:
        
        >>> resource.set_pre()
    """
    global _cwd
    _cwd = posixpath.normpath(posixpath.join(*d))

def get_pre():
    """ Returns the path set with get_pre. """
    return _cwd

def get_abs_id(ID):
    """ Returns the true ID string that would be assumed from the given string. """
    return posixpath.normpath(posixpath.join(_cwd, ID))

def get(resID, type=None):
    """ Returns the resource with the given ID.
        Loads the resource explicitly if it has not already been loaded.
        By default, returns None if there isn't a resource registered with the ID or the
        resource has the wrong type ID.
    """
    resID = get_abs_id(resID)
    if not resID in _metadata:
        return None
    md = _metadata[resID]
    return md.get() if (type is None or type == md.type) else None

def depend(resID, type=None):
    """ Same as get, but raises ResourceError instead of returning None when
        the resource is not available.
    """
    resID = get_abs_id(resID)
    if not resID in _metadata:
        raise ResourceError("Resource '%s' not available."%resID)
    md = _metadata[resID]
    if type is not None and md.type != type:
        raise ResourceError("Resource '%s' is not of required type '%s'."
                            %(resID,type))
    return md.get()

def load(resID, type=None):
    """ Loads a new copy of a resource even if already loaded.
    
        This loads the data regardless of whether or not it was already
        loaded.
        
        The RMS does NOT keep track of objects returned by this
        function. This means that loading a resource explicitly through
        this function does not guaruntee that another copy will not be
        loaded implicitly if `get` or `depend` is called later.
        
        Raises ResourceError if the resource with the given ID does not
        exist.
    """
    resID = get_abs_id(resID)
    if not resID in _metadata:
        raise ResourceError("Resource '%s' not available."%resID)
    md = _metadata[resID]
    if type is not None and md.type != type:
        raise ResourceError("Resource '%s' is not of required type '%s'."
                            %(resID,type))
    return md.load()

def get_time(resID):
    " Returns the last modified time of the file for the given resoruce ID. "
    resID = get_abs_id(resID)
    _metadata[ID].get_time()

def get_exists(resID):
    ' Returns whether or not there is a resource registered with the given ID. '
    resID = get_abs_id(resID)
    return resID in _metadata

def get_type(resID):
    """ Returns the type of the resource with the given ID.
        A KeyError will be raised if the resource isn't registered.
    """
    resID = get_abs_id(resID)
    return _metadata[resID].type

def get_metadata(resID):
    """ Returns the metadata object with the given ID.
        (If you want to be hacky you can also use resource._metadata[resID].
        However, don't rely on this too heavily because it could change
        later.)
        Returns None if no metadata with the given Resource ID exists.
    """
    resID = get_abs_id(resID)
    if resID not in _metadata:
        return None
    return _metadata[resID]

def add_metadata(resID, typeID, filename, **extra):
    """ Catalogs a resource by creating and registering a MetaData for it.
        
        resID:    The resource's unique ID as a string.
        typeID:   The ID of the type of the resource.
        filename: Name of file from which to load the resource.
            
        You can specify a set of extra keyword arguments. Data passed here
        may be used by resource autoloaders.
    """
    resID = get_abs_id(resID)
    md = MetaData(resID,typeID,filename,**extra)
    _register_metadata(md)
    return md

class MetaData(object):
    """ A metadata object performs resource management for a single resource.
    
        After a metadata is registered into the library the resource can be
        dynamically loaded and unloaded using the resource management system
        through the metadata.
        
        After an metadata is created, you don't normally need to access the metadata
        directly after it. Instead, you use the various functions of the
        resource module that access the appropriate methods of the metadata.
    """
    
    def __init__(self, id, type, src, e=None, **extra):
        """ Initializes a new MetaData object.
        
            id:   The resource's unique ID as a string.
            type: The ID of the type of the resource.
            src:  Name of file from which to load the resource.
            
            You can specify a set of extra keyword arguments. Data passed here
            may be used by the autoloader used to load the resource.
        """
        self._resID = id
        self._typeID = type
        self.src = src
        self._res = None
        self.extra = e or extra
    
    def load(self):
        """ Loads the resource for this metadata and returns it.
        
            This loads the data regardless of whether or not it was
            already loaded.
            
            The metadata object does NOT keep track of objects returned
            by this method (except when it is called implicitly by the
            `get` method). This means that loading a resource
            explicitly through this method does not guaruntee that
            another copy will not be loaded implicitly if `get` is
            called later.
        """
        return _type[self._typeID](self)
    
    def _notify_delete(self, ref):
        """ Internal function.
            Used as a callback for when the last reference to a resource is
            deleted.
        """
        self._res = None
    
    def get(self):
        """ Returns a reference to the resource for this metadata.
            
            If the resource is not loaded, it is loaded automatically.
            When the last reference to the resource is deleted, the resource
            will be unloaded.
            
            Calling a metadata object like a function is the same as calling
            this method.
        """
        if self._res is not None:
            return self._res()
        else:
            res = self.load()
            self._res = weakref.ref(res,self._notify_delete)
            return res
    
    __call__ = get

    def get_time(self):
        """ Returns the last modified time of the file path associated with the metadata.
            This uses os.path.getmtime(). See the documentation for that function.
        """
        return os.path.getmtime(self.src)

    # Properties
    
    id = property(lambda self: self._resID, None, None,
                  "Unique identifying string for this metadata and resource.")
    
    type = property(lambda self: self._typeID, None, None,
                    "Identifying string of the resource's type.")
